Skip to content

Conversation

@aardvark179
Copy link
Contributor

@aardvark179 aardvark179 commented Nov 17, 2025

Another draft PR on the road to separating scopes and scriptable objects (#2163). I've put the rationale for doing this in a separate issue(#2164)

The global scope in EcmaScript is a a little subtle in that it consists of a declaration environment record and an object environment record for the global object (globalThis). The global object contains variable declarations, function declarations, generator function declarations, async function declarations, and async generator function declarations. Everything else (const declarations, let declarations, class declarations, etc.) goes on declaration environment and is not added to globalThis. This change could make that full separation, but currently there is one test in MozillaSuiteTest which depends on a const declaration being added to the global this. I wonder if we should gate that on a language version.

This is working well apart from a couple of edge cases that I think it's worth talking through:

  1. ImporterTopLevel is mostly working, but I don't think I have a great model of how it should work. Currently The behaviour for package lookup is living on the scope half, but I'm wondering if ImporterTopLevel should really put all the behaviour on a custom GlobalThis.
  2. One doc test is using a java method to get the XML implementation associated with the current global scope. Currently that is not part of globalThis, but maybe like java imports it should live on that half.
  3. Module scopes. These have some existing awkward special handling on FunctionObject (which is currently the only way the existing customGlobal test works). The existing ModuleScope model also doesn't match require in node or common-js.

Let me expand on that last point slightly. If I have a file blah.js containing

function thing() { return this; }
exports.thing = thing;
exports.thing2 = thing();

and from node REPL I do

e = require("./blah.js");
thing = e.thing
thing2 = e.thing2
function thing3() { return this; }
thing4 = thing3();

Then we find thing2 === globalThis and thing2 === thing4. require simply introduces a new scope (just like a function does) rather than what we do with a module scope (a new top level scope that happens to have another top level scope as the prototype). What we have doesn't match that, but it also doesn't match the behaviour of import which should have an entirely separate realm (top level scope).

I think I can see two possible ways to resolve this:

  1. If you want custom function provided by a java class on your scope then that java object has to be your globalThis, and the module scope becomes a normal child scope. This would be closest to node as far as I can tell. We might well want to keep the ModuleScope as a special class to mark that we are in a module for relative requires etc.
  2. We keep module scope as a top level scope, but with a globalThis of whatever custom type provided. To avoid mutating the main globalThis when executing code in a module we'd need to ensure all modules execute on child scopes of this main module scope (but those need not themselves be ModuleScopes, and we'd need to find all instanceof ModuleScope checks which currently look at the thisObj and change them to appropriate top scope checks.

I don't know if @youngj is still around, still has their use case, and has an opinion on this. I can't tell how well covered this area really is because our Jacoco reports in rhino only cover the tests in rhino, while the coverage report in tests doesn't seem to be configured to show the core runtime classes.

@aardvark179
Copy link
Contributor Author

Okay, I've almost got this sorted. There remain a small set of failing tests due to nested scopes and associated values, which think I can tackle. This has required an API change in how we create nested scopes from a common sealed parent. I'm going to try and distill that down to a simple API, maybe something like public Toplevel createIsolate() and public TopLevel.createIsolate(ScriptableObject customGlobal) or something like that. Since this would change our documented API we would need to make a change to the docs as well at some point.

@aardvark179 aardvark179 force-pushed the aardvark179-separate-global-this branch from 2fecd46 to 10c5f88 Compare November 19, 2025 15:28
@aardvark179 aardvark179 marked this pull request as ready for review November 19, 2025 15:29
@andreabergia
Copy link
Contributor

One concern I have is that it's not great how in so many places you have to do:

        if (scope instanceof TopLevel) {
            scope = ((TopLevel) scope).getGlobalThis();
        }

I am unsure on how we could improve it, but it's really diffused basically everywhere we have Function.call. Feels easy to forget in some place, especially for new code added in the future.

I am not sure why you are doing all the delegation in TopLevel to the globalThis. I have some ideas, but if you could draw a diagram of the scope structure, it would be very helpful for me.

@aardvark179
Copy link
Contributor Author

aardvark179 commented Nov 21, 2025

@andreabergia I agree that I don't really like the tests for top level that are now scattered through several bits of initialisation code. The original reason to not force the top scope to be a TopLevel was that we had historically allowed custom objects to be used, but I think the separation here allows users to have a custom object as their global this and still force the top level scope to be an actual TopLevel. This would certainly help make behaviour consistent with respect to generator functions and other objects whose prototypes are never created as normal properties of the global scope.

I've prototyped this by doing it in stages

  1. Making the object we create if null is provided as the scope.
  2. Throwing an error if we are passed something that isn't a TopLevel as the scope
  3. Then changing the type.

I'm currently on step 2. and have had make small adjustments to four tests, and have another four which seem to need similar fixes. @gbrail how would you feel about this API change?

@aardvark179
Copy link
Contributor Author

Okay, I've moved all the initialisation stuff over to requiring a TopLevel, and it wasn't a big change really. I still have one failing test case due to serialisation and class caches. I'll investigate that over the weekend.

There are still couple of explicit checks for TopLevel which I'm also going to try and work through to see which can reasonably be removed without causing too much disruption.

@aardvark179
Copy link
Contributor Author

Well, that went well. 😆 so it turns out that even if we force the creation of all top level scopes to be actual TopLevels that isn’t enough because of two things

  1. Objects that are created to be scopes but have their parent scope set to null.
  2. Places where we manage to swap the scope and the global this In our code.

The serialisation problem is similar in that the relationship between the top level scope and the global this has been fully set up at the point when something is trying to rely on that.

I’m setting this back to draft until I can track down those problems.

@aardvark179 aardvark179 marked this pull request as draft November 23, 2025 18:58
@aardvark179
Copy link
Contributor Author

Had a burst of inspiration this morning and got this down to about 30 failing test cases. I think all the remaining cases are actual bugs where we ended up with TypeInfoFactory cached against random objects that hadn't had their scopes set up yet. There's a couple of APIs around defining properties which need fixing, it turns out I even did those fixes on my prototype branch for scopes, so I'll try and bring those across to here.

The serialisation problem is occurring because of the way I handled associated values on the top level scope. To enable the creation of isolates I put the values on the global object rather than the scope, and relied on being able to reach that object before it has been associated with top level scope, so the lookup breaks.

I think the thing to do here is to delegate associated values explicitly when creating isolates. I think two top level scopes created from a sealed shared scope should be guaranteed to share the same type cache.

@gbrail
Copy link
Collaborator

gbrail commented Nov 25, 2025

I feel like I'm falling behind here, and apologies -- but it's a testament to the great work you're all doing.

I want to think more carefully about what we're doing to backward compatibility with all this stuff. I've never been super happy about the whole "parent scope" thing, but it's a fundamental part of Rhino and creating a "sealed shared scope" is used by a lot of examples and docs and probably real projects.

Can we document somewhere what we're proposing that will require people to change their current code, and then make a plan to stage it in upcoming releases?

Between this and other things we're working on, as well as the increased velocity of work on the project, it may indeed be time for a 1.9.0 or even a 2.0 with some reasonable changes to backward compatibility in favor of spec compliance.

@p-bakker
Copy link
Collaborator

Just a possible heads-up when doing stuff in the area of sealed/shared scopes: #1085

@aardvark179
Copy link
Contributor Author

@gbrail Strongly agree. I think we're approaching the point where we want to do a release prior to any significant API change, and then work to get all those API changes into a 2.0.0 release. I think it might be worth it to create a 2.0 feature branch which we use for this and regularly rebase it on top of master.

I understand why people want to create a sealed parent object as it is a reasonably simple way to avoid repeated initialisation and prevent accidental modification, and we should absolutely ensure there is an API for doing so. I think that can be done, ad we can make it make it easier to use than the current setPrototype/setParentScope dance. That would give people a simple way to do this which we could encourage them to move to, and would hide most of the specifics allowing us to refactor things internally.

@aardvark179 aardvark179 force-pushed the aardvark179-separate-global-this branch from 890f88d to 49f3a22 Compare November 28, 2025 16:48
@aardvark179
Copy link
Contributor Author

Okay. Looks like there is a small change required for some Android tests and a little tidy up required. I'll try and do that over the weekend.

@aardvark179 aardvark179 force-pushed the aardvark179-separate-global-this branch 3 times, most recently from c161013 to 7ee1037 Compare December 1, 2025 18:30
@aardvark179 aardvark179 marked this pull request as ready for review December 1, 2025 18:31
@rbri
Copy link
Collaborator

rbri commented Dec 1, 2025

Puh, this is huge (and i have to attend a workshop the next days). Will try it next week - hope that is ok

And a question - my top level scope is the window (or the globalworker)

  • Window extends EventTarget
  • EventTarget extends HtmlUnitScriptable
  • HtmlUnitScriptable extends ScriptableObject

Do i have to make Window extending TopLevel?

@aardvark179
Copy link
Contributor Author

Your Window should become the globalThis of a TopLevel, so you should be able to do something like var scope = new TopLevel(new Window) or use the createIsolate API if you want to make a sealed parent.

@aardvark179 aardvark179 force-pushed the aardvark179-separate-global-this branch from 7ee1037 to 82bcce9 Compare December 4, 2025 17:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants